Skip to content

refactor(server): tighten agent runtime + interactive SPI seams - #1091

Closed
FelixTJDietrich wants to merge 1 commit into
post-1081-correctness-sweepfrom
spi-hygiene-tighten
Closed

refactor(server): tighten agent runtime + interactive SPI seams#1091
FelixTJDietrich wants to merge 1 commit into
post-1081-correctness-sweepfrom
spi-hygiene-tighten

Conversation

@FelixTJDietrich

Copy link
Copy Markdown
Collaborator

Summary

Stacked on PR #1090 (post-1081-correctness-sweep). Tightens four architectural seams stretched by #1078 / #1080 / #1081 while the context is fresh — every change is mechanical, behaviour-preserving, and gated by a test.

1. PiRuntimeFactory.build is no longer a god method

The 160-line build() previously branched on runner kind via filename match (MENTOR_RUNNER_SCRIPT constant + nodeFlagsFor/nodeEnvFor/shouldRegisterHephaestusProvider switches). Adding a third runner kind meant editing four switches in the kernel.

  • New PiRunnerProfile interface in agent.runtime; implementations PracticeRunnerProfile (in agent.practice) and MentorRunnerProfile (in agent.mentor) own the runner-specific V8 flags + per-process env next to their adapters.
  • Kernel now reads three properties off the profile; the filename-match dispatch is gone.
  • shouldRegisterHephaestusProvider(spec) deliberately stays a kernel-side helper — it's a function of credentials/baseUrl, not runner kind, and both profiles compute it identically.
  • The profile interface is non-sealed: a sealed ... permits PracticeRunnerProfile, MentorRunnerProfile would force agent.runtime to import its domain implementations — exactly the dependency the boundary rules forbid. Enforced architecturally instead via a new PiRunnerProfilePlacement rule, mirroring the established ContentProvider pattern.
  • Removed the now-redundant runnerScript field from PiAgentProperties / MentorAgentProperties — it decoupled the script filename from the V8 flags / per-process env it must pair with.

2. Per-thread frame routing pushed into the SPI

MentorRunnerClient used to filter inbound frames by boundThreadId inside handleEvent. A future logical-multiplex caller would have had to reinvent the same wheel.

  • New AttachedSandbox.subscribe(Cursor, Predicate<JsonNode>, Consumer) with server-side filtering: the predicate runs inside FrameSubscription.offer before queue enqueue, so rejected frames consume neither queue capacity nor drop-counter budget.
  • The two-arg form becomes a default delegating to frame -> true.
  • MentorFrameFilters.forThread(threadId) encapsulates the JSON-RPC-aware filter — broadcast frames (notifications without a params.threadId) cross every filter; only frames with a mismatched threadId are dropped.
  • SPI Javadoc codifies the broadcast contract.

3. JsonlStdinWriter no longer uses synchronized under virtual threads

Mentor's send() runs on virtual threads; the previous synchronized blocks risked JEP 444 pinning the carrier OS thread for the full critical section (incl. inner queue ops). Replaced with a ReentrantLock — mirrors the precedent set by MentorSseChannel#writeLock. Behaviour identical; pin risk gone.

4. PiPlanSpec.extraInputs path-validation guard

Adapters could previously write to arbitrary workspace paths. Now extraInputs keys must be either:

Violations fail at construction time (boot / first unit test) with a message naming the offending path. Forces new mount points to be declared on WorkspaceAbi.

Test plan

  • ./mvnw test -Dsurefire.includedGroups="unit,architecture"2683 tests (2556 unit + 127 architecture), all green.
  • New tests: PiRuntimeFactoryTest parametrised on both profiles, MentorFrameFiltersTest, AttachedSandboxFilterTest, JsonlStdinWriterConcurrencyTest, PiPlanSpecValidationTest.
  • Verification gates:
    • git grep -n "synchronized" server/application-server/src/main/java/de/tum/in/www1/hephaestus/agent/sandbox/docker/interactive/JsonlStdinWriter.java → 0
    • git grep -n "MENTOR_RUNNER_SCRIPT" server/application-server/src/main/java/de/tum/in/www1/hephaestus/agent/runtime/ → 0
  • AgentRuntimeBoundaryTest extended with PiRunnerProfilePlacement rule — passes.
  • No backwards-compat shims; one commit.

Stacking notes

Base branch is post-1081-correctness-sweep (PR #1090). The Cursor enum + MENTOR_SYSTEM_PROMPT_PATH constant + correctness fixes from that PR are dependencies, not duplicated here.

🤖 Generated with Claude Code

Four architectural seams stretched but not torn by #1081. Tighten now while
the context is fresh.

- PiRuntimeFactory.build was a 160-line god method branching on runner kind:
  replace with a PiRunnerProfile strategy (PracticeRunnerProfile,
  MentorRunnerProfile) owned next to each adapter. The kernel reads three
  properties off the profile (runnerScript, nodeFlags, additionalEnv) instead
  of dispatching on filename. shouldRegisterHephaestusProvider stays a
  spec-helper — it depends on credentials/baseUrl, not runner kind. The
  profile interface is intentionally non-sealed: permits across packages
  would re-introduce the agent.runtime ↛ agent.practice/agent.mentor
  dependency the boundary rules forbid. Enforced architecturally via a new
  PiRunnerProfilePlacement rule, mirroring the ContentProvider pattern.
  Removed the unused operator-overridable runnerScript fields from
  PiAgentProperties / MentorAgentProperties — they decoupled the script
  filename from the V8 flags / per-process env it must pair with.
- Per-thread frame routing was consumer-side filtering inside
  MentorRunnerClient. Push it into the SPI as
  AttachedSandbox.subscribe(Cursor, Predicate<JsonNode>, Consumer): the
  predicate runs server-side before queue enqueue, so suppressed frames
  consume neither queue capacity nor drop-counter budget. Broadcast frames
  (notifications without a thread destination) cross every filter — codified
  in MentorFrameFilters.forThread + SPI Javadoc. The two-arg overload
  remains as a default delegating to an always-true filter.
- JsonlStdinWriter used synchronized under virtual-thread callers; replace
  with a ReentrantLock (mirrors MentorSseChannel#writeLock). JEP 444 pin
  risk removed without changing the enqueue-vs-terminal invariant.
- PiPlanSpec.extraInputs accepted arbitrary workspace paths; validate at
  construction against WorkspaceAbi.allowedExtraInputPaths +
  allowedExtraInputPrefixes. Adapters caught writing to undeclared paths
  fail at boot/test time with a message naming the offending path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@FelixTJDietrich
FelixTJDietrich requested a review from a team as a code owner May 16, 2026 23:00
@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a97d49e-4a63-4ad4-88e9-c02ebe3ba7b6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch spi-hygiene-tighten

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@FelixTJDietrich

Copy link
Copy Markdown
Collaborator Author

Folding into #1086 + #1088 — SPI hygiene split between mentor-runtime (#1086) and runtime-kernel (#1088).

@github-actions

Copy link
Copy Markdown
Contributor

📚 Documentation Preview

Preview has been removed (PR closed)

FelixTJDietrich added a commit that referenced this pull request May 17, 2026
Items folded in from closed PRs #1090, #1091 — they belong here with the
Pi runner config staging consolidation, not as standalone follow-ups.

- WorkspaceAbi.MENTOR_SYSTEM_PROMPT_PATH constant; MentorPiAdapter writes
  through it (no more bare string literal scattered across Java + JS)
- WorkspaceAbiSyncTest extended to scan pi-mentor-runner.mjs; envelope
  mismatch exits 42 (matching EXIT_ENVELOPE_MISMATCH) rather than the
  legacy hardcoded 2
- PiRunnerProfile (non-sealed; preserves runtime ↛ practice/mentor
  boundary) — PracticeRunnerProfile + MentorRunnerProfile own their own
  node flags, env, provider-registration. PiRuntimeFactory.build()
  delegates, no more switch-on-kind god method
- TaskSealedHierarchyTest restored as architecture-tag guardrail against
  permit/JsonSubTypes drift; Task.PracticeReview gains its @JsonTypeName

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
FelixTJDietrich added a commit that referenced this pull request May 17, 2026
Items folded in from closed PRs #1089, #1090, #1091 — they belong here
alongside the sticky-cookie + replica-affinity work, not as standalone
follow-ups.

- MentorReplicaAffinityCheck HealthIndicator (refuses to start on
  multi-replica without operator-asserted sticky routing)
- WorkspaceContextBuilder mentor striping: hash (contributorId, workspaceId)
  instead of returning null and collapsing every mentor session onto
  stripe 0
- AttachedSandbox.subscribe(Cursor) — collapse the default-method
  subscribeFromNow seam; reused sandboxes were replaying prior turn's
  agent_end
- Per-thread frame routing pushed into the SPI as Predicate<JsonNode>
- JsonlStdinWriter synchronized → ReentrantLock (JEP 444 pin risk under
  virtual-thread callers)
- FrameRingBuffer dropped_total metric with bounded cardinality
- EvictionReason.MAX_LIFETIME wired (165 MB RSS × N idle users)
- MentorChatMetrics.Outcome.CAPACITY_EXCEEDED so dashboards stop being
  blind to per-user-cap rejections

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant